Pythonデコレーター 3:デコレーターベースのビルドシステム
この資料を執筆時点で、彼の記事は13年前のものになりますが、未だに有益な資料です。
Python コードについては、Python3 で動作するように修正しています。
著者: Bruce Eckel
2008年10月26日
概要
ほとんどのビルドシステムは、まず依存関係から始まり、次に言語機能が必要であることに気づき、最終的には言語設計から始めるべきであったことに気づくことになります。
私は長年、makeを使ってきました。Antを使ったのは、Javaのビルドが速くなったからです。しかし、どちらのビルドシステムも最初は問題が単純だと思っていましたが、後になってビルド問題を解決するためにはプログラミング言語が必要だということに気づきました。その時にはもう手遅れでした。その結果、物事を成し遂げるために複雑で煩わしい一連のことを行う必要がありました。
言語の上にビルドシステムを構築する試みがなされています。RakeはRubyの上に構築された ドメイン固有言語(Domain-Specific Language: DSL)で、かなりの成功を収めています。また、Pythonでも多くのプロジェクトが作られています。
私は何年もの間、Pythonの上に薄い化粧を施しただけのシステムを望んでいました。つまり、依存関係のサポートは得られますが、それ以外は事実上すべてPythonなのです。そうすれば、PythonとPython以外の言語との間を行ったり来たりする必要がなく、精神的な混乱も少なくて済みます。
デコレーターはこの目的に最適であることがわかりました。ここで紹介しているデザインは最初の切り口に過ぎませんが、新しい機能を追加するのは簡単ですし、私はすでに The Python Book のビルドシステムとして使い始めているので、おそらくもっと機能を追加する必要があるでしょう。最も重要なのは、自分がやりたいことが何でもできるということで、これはmakeやantでは必ずしもそうではありません(確かにantを拡張することはできますが、参入コストが利益に見合わないことが多いのです)。 この本の他の部分にはCreative Commons Attribution-Share Alikeライセンスが付いていますが、このプログラムにはCreative Commons Attributionライセンスしか付いていません。これはどんな状況でも利用してもらいたいからです。もちろん、あなたが何か改良を加えた場合には、それをプロジェクトに還元してくれることが理想的ですが、これはコードを使用したり変更したりするための前提条件ではありません。
構文
ビルドシステムが提供する最も重要で便利なものは、依存関係です。何が何に依存しているか、そしてその依存関係をどのように更新するかを指示します。これをまとめてルールと呼ぶので、デコレータもルールと呼ぶことにします。デコレーターの第一引数はターゲット(更新する必要があるもの)で、残りの引数は依存関係です。ターゲットが依存関係で古くなっている場合は、関数コードを実行して最新の状態にします。
ここでは、基本的な構文を示す簡単な例を紹介します。
code: python
@rule("file1.txt")
def file1():
"File doesn't exist; run rule"
file("file1.txt", 'w')
ruleの名前が file1 なのは、それが関数名だからです。この場合、ターゲットは「file1.txt」であり、依存関係はありません。そのため、ルールはfile1.txtが存在するかどうかをチェックするだけで、存在しない場合は関数コードを実行して最新の状態にします。
docstringを使用していることに注意してください。これは、ビルドシステムによってキャプチャされ、コマンドラインで build help(またはビルダーが理解できない他のもの)と実行したときのルールを記述します。
ruleデコレーターは、添付された関数にのみ影響を与えるので、同じビルドファイル内で通常のコードとルールを簡単に混在させることができます。ここでは、ファイルの日付スタンプを更新したり、ファイルが存在しない場合にファイルを作成したりする関数を紹介します。
code: python
def touchOrCreate(f): # Ordinary function
"""ファイルを最新の状態にし、存在しない場合は作成する"""
if os.path.exists(f):
os.utime(f, None)
else:
file(f, 'w')
より典型的なルールは、ターゲットファイルと1つまたは複数の依存ファイルを関連付けるものです。
code: python
@rule("target1.txt","dependency1.txt","dependency2.txt","dependency3.txt")
def target1():
"""target1.txtの依存関係を最新の状態にする"""
touchOrCreate("target1.txt")
また、このビルドシステムでは、ターゲットをリストに入れることで、複数のターゲットを設定することができます。
code: python
def multipleBoth():
"""複数のターゲットと依存関係"""
ターゲットや依存関係がない場合、ルールは常に実行されます。
code: python
@rule()
def clean():
"""作成されたファイルを削除"""
alFilesの配列は、後で紹介する例で見られます。
他のルールに依存するルールを書くことができます。
code: python
@rule(None, target1, target2)
def target3():
"""ターゲット1とターゲット2を常に最新の状態にする"""
print target3
Noneがターゲットなので、比較するものはありませんが、ルール target1とtarget2をチェックする過程で、それらは両方とも最新の状態になります。これは、例題で見るように、「すべて」のルールを書くときに特に有効です。
ビルダーコード
デコレーターといくつかの適切なデザインパターンを使用することで、コードは非常に簡潔になります。__main__ のコードでは、例となる build.py ファイル(上で見た例などを含む)が作成され、初めてビルドを実行したときには、Windows用のbuild.bat ファイルとUnix/Linux/Cygwin用のbuildコマンドファイルが作成されることに注意してください。完全な説明はコードの後にあります。
code: python
# builder.py
import sys, os, stat
"""
makeなどの代わりに、Python上でビルドルールを追加します。
Adds build rules atop Python, to replace make, etc.
by Bruce Eckel
License: Creative Commons with Attribution.
"""
def reportError(msg):
print("Error:", msg, file=sys.stderr)
sys.exit(1)
class Dependency(object):
"""単一の依存関係を表すためにデコレーターによって作成されます"""
changed = True
unchanged = False
@staticmethod
def show(flag):
if flag: return "Updated"
return "Unchanged"
def __init__(self, target, dependency):
self.target = target
self.dependency = dependency
def __str__(self):
return "target: %s, dependency: %s" % (self.target, self.dependency)
@staticmethod
def create(target, dependency): # Simple Factory
if target == None:
return NoTarget(dependency)
if type(target) == str: # 文字列はファイル名
if dependency == None:
return FileToNone(target, None)
if type(dependency) == str:
return FileToFile(target, dependency)
if type(dependency) == Dependency:
return FileToDependency(target, dependency)
reportError(f'No match found in create() for target: {target}, dependency: {dependency}')
def updated(self):
"""
これが最新であるかどうかを判断するための呼び出しです。
自分で更新しなければならない場合は「changed」を返します。
"""
assert False, "Must override Dependency.updated() in derived class"
class NoTarget(Dependency): # Always call updated() on dependency
def __init__(self, dependency):
Dependency.__init__(self, None, dependency)
def updated(self):
if not self.dependency:
return Dependency.changed # (None, None) -> always run rule
return self.dependency.updated() # Must be a Dependency or subclass
class FileToNone(Dependency): # ファイルが存在しないときのルール
def updated(self):
if not os.path.exists(self.target):
return Dependency.changed
return Dependency.unchanged
class FileToFile(Dependency): # ファイルのタイムスタンプを比較
def updated(self):
if not os.path.exists(self.dependency):
reportError("%s does not exist" % self.dependency)
if not os.path.exists(self.target):
return Dependency.changed # ファイルが存在しないとき make を実行
if os.path.getmtime(self.dependency) > os.path.getmtime(self.target):
return Dependency.changed
return Dependency.unchanged
class FileToDependency(Dependency): # 依存関係にあるオブジェクトが変更された場合>に更新
def updated(self):
if self.dependency.updated():
return Dependency.changed
if not os.path.exists(self.target):
return Dependency.changed # ファイルが存在しないとき make を実行
return Dependency.unchanged
class rule(object):
"""
関数をビルドルールに変えるデコレーターです。
デコレーターの arglist に含まれる最初のファイルまたはオブジェクトが
ターゲットとなり、残りは依存関係となります。
"""
rules = []
default = None
class _Rule(object):
"""
Command pattern. name, dependencies, ruleUpdater and description are
all injected by class rule.
"""
def updated(self):
self.ruleUpdater()
return Dependency.changed
return Dependency.unchanged
def __str__(self): return self.description
def __init__(self, *decoratorArgs):
"""
このコンストラクタは、デコレーションされた関数が定義されたときに
最初に呼び出され、デコレータ自身に渡される引数を捕捉します。
(Note Builder pattern)
"""
self._rule = rule._Rule()
decoratorArgs = list(decoratorArgs)
if decoratorArgs:
if len(decoratorArgs) == 1:
decoratorArgs.append(None)
target = decoratorArgs.pop(0)
if type(target) != list:
self._rule.dependencies = [Dependency.create(targ, dep)
for targ in target for dep in decoratorArgs]
else: # 引数なし
def __call__(self, func):
"""
これは、コンストラクタの直後に呼び出され、
デコレートされる関数オブジェクトが渡されます。
返された _rule オブジェクトが元の関数を置き換えます。
"""
reportError("@rule name %s must be unique" % func.__name__)
self._rule.name = func.__name__
self._rule.description = func.__doc__ or ""
self._rule.ruleUpdater = func
rule.rules.append(self._rule)
return self._rule # デコレートされた関数として置き換えられます。
@staticmethod
def update(x):
if x == 0:
if rule.default:
return rule.default.updated()
else:
return rule.rules0.updated() # Look up by name
for r in rule.rules:
if x == r.name:
return r.updated()
raise KeyError
@staticmethod
def main():
"""
コマンドラインの振る舞いを定義
"""
if len(sys.argv) == 1:
print(Dependency.show(rule.update(0)))
try:
print(Dependency.show(rule.update(arg)))
except KeyError:
print("Available rules are:\n")
for r in rule.rules:
if r == rule.default:
newline = " (Default if no rule is specified)\n"
else:
newline = "\n"
print("%s:%s\t%s\n" % (r.name, newline, r))
print("(Multiple targets will be updated in order)")
# WindowsとUnix用の "build" コマンドを作成
if not os.path.exists("build.bat"):
open("build.bat", 'w').write("python build.py %1 %2 %3 %4 %5 %6 %7")
if not os.path.exists("build"):
# Unless you can detect cygwin independently of Windows
open("build", 'w').write("python build.py $*")
os.chmod("build", stat.S_IEXEC)
############### Test/Usage Examples ###############
if __name__ == "__main__":
if not os.path.exists("build.py"):
open("build.py", 'w').write('''\
# 使用例:テストコードと用法の両方
from builder import rule
import os
@rule("file1.txt")
def file1():
"ファイルが存在しないため、ルールを実行する"
open("file1.txt", 'w')
def touchOrCreate(f): # Ordinary function
"ファイルを最新の状態にし、存在しない場合は作成する"
if os.path.exists(f):
os.utime(f, None)
else:
open(f, 'w')
dependencies = ["dependency1.txt", "dependency2.txt",
"dependency3.txt", "dependency4.txt"]
allFiles = targets + dependencies
@rule(allFiles)
def multipleTargets():
"複数のファイルが存在しないため、ルールを実行する"
def multipleBoth():
"複数のターゲットと依存関係"
@rule("target1.txt","dependency1.txt","dependency2.txt","dependency3.txt")
def target1():
"target1.txtの依存関係を最新の状態にする"
touchOrCreate("target1.txt")
@rule()
def updateDependency():
"すべてのdependency.*ファイルのタイムスタンプを更新する"
@rule()
def clean():
"作成したファイルをすべて削除する"
@rule()
def cleanTargets():
"ターゲットファイルをすべて削除"
@rule("target2.txt", "dependency2.txt", "dependency4.txt")
def target2():
"target2.txtの依存関係を最新の状態にしたり、作成したりする"
touchOrCreate("target2.txt")
@rule(None, target1, target2)
def target3():
"常にターゲット1とターゲット2を最新の状態にする"
print(target3)
@rule(None, clean, file1, multipleTargets, multipleBoth, target1,
updateDependency, target2, target3)
def all():
"すべてを最新の状態にする"
print(all)
rule.default = all
rule.main() # ビルドの実行、コマンドライン引数の処理
''')
最初のクラス群は、異なるタイプのオブジェクト間の依存関係を管理します。ベースクラスは、派生クラスで明示的に再定義されていない場合に自動的に呼び出されるコンストラクタ(Pythonのコード削減のための素晴らしい機能)を含むいくつかの共通コードを含んでいます。
訳注:オリジナルのコードは、open() のかわりに file() を使用していました。これは、Python 2.2 から導入された組み込み関数で open() とほとんど同じ動作をするものですが、Python 3.0 で削除されています。
Dependencyから派生したクラスは、特定のタイプの依存関係を管理し、ターゲットが依存関係を最新の状態にするべきかどうかを決定するためにupdated() メソッドを再定義します。これはテンプレートメソッドデザインパターンの例で、updated() はテンプレートメソッド、_Rule はコンテキストです。
依存関係やターゲットにワイルドカードを追加するなど、新しいタイプの依存関係を作成したい場合は、新しいDependencyサブクラスを定義します。コードの残りの部分は変更を必要としないことがわかります。これはデザインにとってポジティブな指標です(将来の変更が隔離される)。
Dependency.create() は私がシンプル・ファクトリー・メソッドと呼んでいるもので、Dependencyのすべてのサブタイプの生成をローカライズしているだけだからです。ある言語のように前方参照が問題になることはないので、Design Patternsで与えられたファクトリーメソッドの完全な実装を使用する必要はなく、またより複雑であることに注意してください(これは、完全なファクトリーメソッドを正当化するケースがないという意味ではありません)。
なお、FileToDependencyではself.dependencyがDependencyのサブタイプであることを主張することができますが、この型チェックは(実質的に)updated() が呼ばれたときに行われます。
訳注: Design Patternsで与えられたファクトリーメソッドの完全な実装を使用する必要はなく(原文:"so using the full implementation of Factory Method given in GoF is not necessary and also more complex")は意訳しています。
原文にある GoF はコンピュータ関連では Gang og Four(GoF) のことで、書籍『オブジェクト指向における再利用のためのデザインパターン』(原題:Design Patterns: Elements of Reusable Object-Oriented Software) の著者である、Erich Gamma、Richard Helm、Ralph E. Johnson、John Matthew Vlissides の4人を指します。
ルールデコレーター
ルールデコレーターは Builder デザインパターンを採用しています。 これは、ルールの作成が 2 つのステップで行われるからです。 コンストラクタはデコレーターの引数を受け取り、 __call__() メソッドは関数を受け取ります。
Builderが生成するものは _Rule オブジェクトで、Dependencyクラスと同様にupdated() メソッドを含んでいます。各_Ruleオブジェクトには、依存関係のリストと、依存関係のどれかが古くなった場合に呼び出される ruleUpdater() メソッドが含まれています。また、_Rule には名前(デコレートされた関数の名前)と説明(デコレートされた関数のdocstring)が含まれています。(_RuleオブジェクトはCommandパターンの一例です)。
_Ruleで変わっているのは、依存関係、ruleUpdater()、name、description を初期化するコードがクラス内に見当たらないことです。これらは、注入(インジェクション:Injection) を使用して、 Builderプロセス中にruleによって初期化されます。これに代わる典型的な方法はセッター・メソッドを作成することですが、_Ruleはruleの中にネストされているため、ruleは実質的に_Ruleを「所有」しており、インジェクションの方がはるかに簡単に思えます。
ruleのコンストラクタでは、まず生成物である_Ruleオブジェクトを作成し、次にデコレータの引数を処理します。decoratorArgsをリストに変換するのは、変更可能である必要があるからで、 decoratorArgsはタプルとして入ってきます。引数がひとつしかない場合は、ユーザーがターゲットのみを指定し、依存関係がないことを意味します。Dependency.create() は2つの引数を必要とするので、Noneをリストに追加します。
target は常に最初の引数なので、pop(0)でそれを取り出したリストの残りは依存関係です。ターゲットがリストである可能性に対応するために、単一のターゲットはリストに変えられます。
ここで、Dependency.create() がターゲットと依存関係の可能な組み合わせごとに呼び出され、結果として得られるリストが_Ruleオブジェクトに注入されます。引数がない特殊なケースでは、None to None Dependencyが作成されます。
ruleコンストラクタが行うことは、引数を整理することだけで、特定の関係については何も知らないことに注意してください。これにより、特別な知識はDependency の階層内に保たれ、新しいDependencyの追加はその階層内で分離されます。
同様のガイドラインは、装飾された関数をキャプチャする __call__() メソッドにも適用されます。_Rule オブジェクトをrules という静的なリストに入れておき、まず最初にルール名が重複していないかどうかをチェックします。そして、name、docstring、そして関数自体をキャプチャして注入します。
Builderの生成物である_Ruleオブジェクトが、rule.__call__() の結果として返されていることに注意してください。これは、__call__() メソッドを持たないこのオブジェクトが、デコレーションされた関数の代わりになっていることを意味しています。通常、装飾された関数は直接呼び出されますが、この場合、装飾された関数は直接呼び出されず、_Rule オブジェクトを介してのみ呼び出されます。
ビルドの実行
rule の静的メソッド main() は、ヘルパー・メソッド update() を使ってビルド処理を行います。コマンドライン引数を指定しない場合、main() は 0 を update() に渡し、デフォルトのルールが設定されている場合はそれを、そうでない場合は定義された最初のルールを呼び出します。コマンドライン引数を指定した場合は、それぞれの引数を(順番に)update() に渡します。
間違った引数を与えた場合(通常は help がそのために用意されています)、各ルールをその docstring と共に表示します。
最後に、build.batとbuildコマンドファイルが存在するかどうかをチェックし、存在しなければ作成します。
builder.py を初めて実行したときに生成される build.py は、あなたのビルドファイルの出発点として機能します。
改善点
現状では、このシステムは基本的なニーズを満たしているに過ぎません。例えば、依存関係を操作するためのmakeのような機能はありません。一方で、このシステムは強力なプログラミング言語の上に構築されているため、必要なことは何でも簡単にできます。もし、同じコードを何度も書いているようなら、rule()を修正して、重複する作業を減らすことができます。もし許可があれば、そのような修正案を提出して、採用される可能性を検討してください。
次回
このシリーズの最終回(章)では、クラスデコレーターと、オブジェクトをデコレーションできるかどうかについて見ていきます。
訳注: 次回について言及がありますが、残念ながら該当する記事は発表されていません。コメント(Talk Back!) と RSS Feed のセクションは割愛しています。
ブロガーについて
https://gyazo.com/b61197ffc968b0ec1e14d040e2fefb74
Bruce Eckel (www.BruceEckel.com) は、Pythonでの開発支援とFlexでのユーザーインターフェースの提供を行っている。Thinking in Java (Prentice-Hall, 1998, 2nd edition, 2000, 3rd edition, 2003, 4th edition, 2005)、Hands-On Java Seminar CD ROM (Webサイトで入手可能)、Thinking in C++ (PH 1995; 2nd edition 2000, Volume 2 with Chuck Allison, 2003)、C++ Inside & Out (Osborne/McGraw-Hill 1993)などの著書があります。また、世界中で何百ものプレゼンテーションを行い、数多くの雑誌に150以上の記事を掲載しているほか、ANSI/ISO C++委員会の創設メンバーでもあり、カンファレンスでも定期的に講演を行っています。